# This is an experimental file, I've been developing to test
# how feesible the use of libxml will be for parsing the proxied html files
# 
# One of the main benifits I see from using libxml is the ability to avoid holding the
# full document in memory on the proxy server. 
#
# However, unlike regex this could expose the 
# server to faults in ways less expected. It also means the document is reassembled on the way
# out and could be subject to breaking depending on whether or not we miss a parse condition
#
# Also, the current libxml-ruby implementation does not appear to support parse streaming, although
# I don't see it as being to big of a challenge to overcome this...
#

require 'rubygems'
gem 'libxml-ruby'
require 'xml/libxml'
require 'thread'
if RUBY_VERSION < '1.9.0'
  gem 'fastthread'
  require 'fastthread'
end
require 'timeout'
require 'uri'
require 'net/http'
require 'yaml'

$:.unshift File.join(File.dirname(__FILE__),'lib')

require 'esi/cache'
require 'esi/router'
require 'esi/tag/base'
require 'esi/tag/include'
require 'esi/tag/attempt'
require 'esi/tag/except'
require 'esi/tag/try'
require 'esi/tag/invalidate'

class ESIParser
  def initialize(cache,router,proxy_headers)
    @output = $stdout
    @esi_tag = nil
    @cache = cache
    @proxy_headers = proxy_headers
    @request_queue = Queue.new # as we encounter esi:include tags we'll push the requests on to this queue
    @router = router
  end

  def output=(o)
    @output = o
  end

  def parse(proxy_input)
    parser = XML::SaxParser.new
    parser.string = proxy_input
    parser.callbacks = self
    parser.parse
  end

  def on_internal_subset(a,b,c)
    buffer "<!DOCTYPE #{a} PUBLIC \"#{b}\" \"#{c}\">\n"
  end

  def on_external_subset(a,b,c)
  end
    
  def on_processing_instruction(target, data)
    buffer target
  end

  def on_cdata_block(cdata)
    buffer "<![CDATA[#{cdata}]]>"
  end
  
  def on_start_document
  end

  def on_end_document
  end

  def on_start_element( name, attrs )
    if( name.match(/esi:/) )
      tag = ESI::Tag::Base.create(@router, @proxy_headers, {}, name, attrs, @cache)
      if @esi_tag
        @esi_tag.add_child(tag)
      else
        @esi_tag = tag
      end
    else
      buffer "<#{name}#{print_attrs(attrs)}>"
    end
  end

  def on_end_element( name )
    if( name.match(/esi:/) )
      if @esi_tag.name == (name.gsub(/esi:/,''))
        @esi_tag.close(@output)
        @esi_tag = nil
      else
        @esi_tag.close_child(@output,name)
      end
    else
      buffer "</#{name}>"
    end
  end
  
  def on_characters( chars )
    buffer chars
  end

  def on_comment( comment )
    buffer "<!--#{comment}-->"
  end

  def on_parser_error( error )
    $stderr.puts error
  end

  def print_attrs(attrs)
    " " + attrs.collect{|k,v| "#{k}=\"#{v}\"" }.join(' ') unless attrs.empty?
  end

  def buffer(string)
    if @esi_tag
      @esi_tag.buffer( @output, string )# unless string.strip.empty?
    else
      @output << string
    end
  end

end

DEBUG=1
if defined?(DEBUG)

require 'webrick'
class ::WEBrick::HTTPServer ; def access_log(config, req, res) ; end ; end
class ::WEBrick::BasicLog ; def log(level, data) ; end ; end

require 'test/unit'

module TestServer
  def setup
    @server = WEBrick::HTTPServer.new( :Port => 99999 )

    # setup test server (simulates exact target)
    @server.mount_proc("/test/error") do|req,res|
      raise "fail"
    end
    @server.mount_proc("/test/redirect") do|req,res|
      res.set_redirect(WEBrick::HTTPStatus::MovedPermanently,"/test/success")
    end
    @server.mount_proc("/test/redirect_to_error") do|req,res|
      res.set_redirect(WEBrick::HTTPStatus::MovedPermanently,"/test/error")
    end
    @server.mount_proc("/test/success") do|req,res|
      id = req.query["id"]
      if id
        res.body = "<div id='#{id}'>id string</div>"
      else
        res.body = "<div>hello there world</div>"
      end
      res['Content-Type'] = "text/html"
    end
    @server.mount_proc("/test/timeout") do|req,res|
      sleep 1.5
      res.body = "<div>We won't get this for a few seconds</div>"
      res['Content-Type'] = "text/html"
    end
    @server.mount_proc("/test/headers") do|req,res|
      res.body = "<div>hello there world</div>"
      res['Content-Type'] = "text/html"
      begin
        assert_equal ['value1'], req.header['headers1']
        assert_equal ['value2'], req.header['headers2']
      rescue Object => e
        puts e.message
        raise e
      end
    end

    # start up the server in a background thread
    @thread = Thread.new(@server) do|server|
      server.start
    end

    setup_extra if self.respond_to?(:setup_extra)
  end
  def teardown
    @server.shutdown
    @thread.join
  end
end

class TestIncludeRequest < Test::Unit::TestCase
  include TestServer

  def request(path,alt=nil,headers={})
    ir = ESI::Tag::IncludeRequest.new(headers)
    buffer = ""
    status = 0
    info = nil
    alt = "http://127.0.0.1:99999#{alt}" if alt
    ir.request("http://127.0.0.1:99999#{path}",0.05,alt) do|s,r|
      if s
        buffer = r.read_body
        status = 1
      else
        info = r.message
        buffer = r.response.read_body if r.response
      end
    end
    [buffer,info,status]
  end

  # happy paths
  def test_success
    ri = request('/test/success')
    assert_equal 1, ri[2], "Request failed\n#{ri[0]}"
    assert_equal %Q(<div>hello there world</div>), ri[0], "Request failed"
  end
  
  def test_redirect
    ri = request('/test/redirect')
    assert_equal 1, ri[2], "Request failed\n#{ri[0]}"
    assert_equal %Q(<div>hello there world</div>), ri[0], "Request failed"
  end

  def test_error_alt
    ri = request('/test/error','/test/success')
    assert_equal 1, ri[2], "Request failed\n#{ri[0]}"
    assert_equal %Q(<div>hello there world</div>), ri[0], "Request failed"
  end
  
  def test_error_no_alt
    ri = request('/test/error')
    assert_equal 0, ri[2], "Request should have failed\n#{ri[0]}"
  end

  # not so happy paths
  def test_error_error_alt
    ri = request('/test/error','/test/error')
    assert_equal 0, ri[2], "Request should have failed\n#{ri[0]}"
  end

  def test_redirect_failed_to_alt
    ri = request('/test/redirect_to_error','/test/redirect_to_error')
    assert_equal 0, ri[2], "Request should have failed\n#{ri[0]}"
  end
  
  def test_request_timesout
    ri = request('/test/timeout')
    assert_equal 0, ri[2], "Request should have failed\n#{ri[0]}"
  end

  def test_headers_forward
    ri = request('/test/headers',nil,{'Headers1' => 'value1', 'Headers2' => 'value2'})
    assert_equal 1, ri[2], "Request not have failed\n#{ri[0]}"
  end
  
end

class TestIncludeTag < Test::Unit::TestCase
  include TestServer
  def test_close
      router = ESI::Router.new(YAML.load(%q(
  default:
    host: 127.0.0.1
    port: 99999) ) )
    include_tag = ESI::Tag::Base.create(router, {}, {},
                                        'include', {'src'=>"/test/success/"},
                                        ESI::RubyCache.new)
    output = ""
    include_tag.close(output)
    assert_equal("<div>hello there world</div>",output)
  end
end

$run_sample_once = false
class TestParseOutput < Test::Unit::TestCase
  include TestServer

  def setup_extra
    if !$run_sample_once   
      input = File.read(File.join(File.dirname(__FILE__),'esi-sample.html'))

      @cache = ESI::RubyCache.new
      router = ESI::Router.new(YAML.load(%q(
  default:
    host: 127.0.0.1
    port: 99999) ) )

      @parser = ESIParser.new(@cache, router, {})

      File.open('out-sample.html','w') do|output|
        @parser.output = output
        @parser.parse( input )
      end

      $run_sample_once = true
    end
    @output=File.read('out-sample.html')
  end

  def test_ouput_body
    assert_match(/  <div class="body">/, @output)
  end
  def test_ouput_some_content
    assert_match(/    <div>some content<\/div>/, @output)
  end
  def test_output_comments
    assert_match(/    <!-- a little commentary -->/, @output)
    assert_match(%q(<!-- a 
      multiline comment -->),@output)
  end
  def test_output_cdata
    assert_match(%q(<![CDATA[
      Some cdata fun fun fun
    ]]>), @output)
  end
  def test_output_doctype
    assert_match(/<!DOCTYPE html PUBLIC "-\/\/W3C\/\/DTD XHTML 1.0 Strict\/\/EN" "http:\/\/www.w3.org\/TR\/xhtml1\/DTD\/xhtml1-strict.dtd">/, @output)
  end
  def test_output_complete
    assert_match(/<\/html>/, @output)
  end
  def test_output_no_esi_tags
    assert_no_match(/<esi:/, @output)
  end

  def test_includes_appeared
    assert_match(/<div id='1'>id string<\/div>/, @output)
    assert_match(/<div id='3'>id string<\/div>/, @output)
    
    assert_no_match(/<p>it failed<\/p>/, @output)
    assert_match(/<div id='2'>id string<\/div>/, @output)

    assert_match(/<div id='4'>id string<\/div>/, @output)
    assert_match(/<p>We should get this or not\?<\/p>/, @output)
    assert_match(/<p>Now maybe we shouldn't see this\?<\/p>/, @output)
    assert_match(/<p>except worked1<\/p>/, @output)
    assert_match(/<p>except worked2<\/p>/, @output)
  end

end
end
